<?php

/*

    Copyright (c) 2009-2019 F3::Factory/Bong Cosca, All rights reserved.

    This file is part of the Fat-Free Framework (http://fatfreeframework.com).

    This is free software: you can redistribute it and/or modify it under the
    terms of the GNU General Public License as published by the Free Software
    Foundation, either version 3 of the License, or later.

    Fat-Free Framework is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    General Public License for more details.

    You should have received a copy of the GNU General Public License along
    with Fat-Free Framework.  If not, see <http://www.gnu.org/licenses/>.

*/
declare(strict_types=1);

namespace Shoofly;

//! Wrapper for various HTTP utilities
class Web extends Prefab
{
    //@{ Error messages
	// phpcs:disable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
    public const E_Request = 'No suitable HTTP request engine found';
	// phpcs:enable Generic.NamingConventions.UpperCaseConstantName.ClassConstantNotUpperCase
    //@}

    protected //! HTTP request engine
        $wrapper;

    /**
    *   Detect MIME type using file extension or file inspection
    *   @return string
    *   @param $file string
    *   @param $inspect bool
    **/
    public function mime($file, $inspect = false)
    {
        if ($inspect) {
            if (is_file($file) && is_readable($file)) {
                // physical files
                if (extension_loaded('fileinfo')) {
                    $mime = mime_content_type($file);
                } elseif (preg_match('/Darwin/i', PHP_OS)) {
                    $mime = trim(exec('file -bI ' . escapeshellarg($file)));
                } elseif (!preg_match('/^win/i', PHP_OS)) {
                    $mime = trim(exec('file -bi ' . escapeshellarg($file)));
                }
                if (isset($mime) && !empty($mime)) {
                    // cut charset information if any
                    $exp = explode(';', $mime, 2);
                    $mime = $exp[0];
                }
            } else {
                // remote and stream files
                if (ini_get('allow_url_fopen') && ($fhandle = fopen($file, 'rb'))) {
                    // only get head bytes instead of whole file
                    $bytes = fread($fhandle, 20);
                    fclose($fhandle);
                } elseif (($response = $this->request($file, ['method' => 'HEAD']))
                    && preg_grep('/HTTP\/[\d.]{1,3} 200/', $response['headers'])
                    && ($type = preg_grep('/^Content-Type:/i', $response['headers']))
                ) {
                    // get mime type directly from response header
                    return preg_replace('/^Content-Type:\s*/i', '', array_pop($type));
                } else { // load whole file
                    $bytes = file_get_contents($file);
                }
                if (extension_loaded('fileinfo')) {
                    // get mime from fileinfo
                    $finfo = finfo_open(FILEINFO_MIME_TYPE);
                    $mime = finfo_buffer($finfo, $bytes);
                } elseif ($bytes) {
                    // magic number header fallback
                    $map = [
                        '\x64\x6E\x73\x2E' => 'audio/basic',
                        '\x52\x49\x46\x46.{4}\x41\x56\x49\x20\x4C\x49\x53\x54' => 'video/avi',
                        '\x42\x4d' => 'image/bmp',
                        '\x42\x5A\x68' => 'application/x-bzip2',
                        '\x07\x64\x74\x32\x64\x64\x74\x64' => 'application/xml-dtd',
                        '\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1' => 'application/msword',
                        '\x50\x4B\x03\x04\x14\x00\x06\x00' => 'application/msword',
                        '\x0D\x44\x4F\x43' => 'application/msword',
                        'GIF\d+a' => 'image/gif',
                        '\x1F\x8B' => 'application/x-gzip',
                        '\xff\xd8\xff' => 'image/jpeg',
                        '\x49\x46\x00' => 'image/jpeg',
                        '\xFF\xFB' => 'audio/mpeg',
                        '\x49\x44\x33' => 'audio/mpeg',
                        '\x00\x00\x01\xBA' => 'video/mpeg',
                        '\x4F\x67\x67\x53\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00' => 'audio/vorbis',
                        '\x25\x50\x44\x46' => 'application/pdf',
                        '\x89PNG\x0d\x0a' => 'image/png',
                        '.{4}\x6D\x6F\x6F\x76\x' => 'video/quicktime',
                        '\x53\x49\x54\x21\x00' => 'application/x-stuffit',
                        '\x43\x57\x53' => 'application/x-shockwave-flash',
                        '\x1F\x8B\x08' => 'application/x-tar',
                        '\x49\x20\x49' => 'image/tiff',
                        '\x52\x49\x46\x46.{4}\x57\x41\x56\x45\x66\x6D\x74\x20' => 'audio/wav',
                        '\xFD\xFF\xFF\xFF\x20\x00\x00\x00' => 'application/vnd.ms-excel',
                        '\x50\x4B\x03\x04' => 'application/x-zip-compressed',
                        '[ -~]+$' => 'text/plain',
                    ];
                    foreach ($map as $key => $val) {
                        if (preg_match('/^' . $key . '/', substr($bytes, 0, 128))) {
                            return $val;
                        }
                    }
                }
            }
            if (isset($mime) && !empty($mime)) {
                return $mime;
            }
            // Fallback to file extension-based check if no mime was found yet
        }
        if (preg_match('/\w+$/', $file, $ext)) {
            $map = [
                'au' => 'audio/basic',
                'avi' => 'video/avi',
                'bmp' => 'image/bmp',
                'bz2' => 'application/x-bzip2',
                'css' => 'text/css',
                'dtd' => 'application/xml-dtd',
                'doc' => 'application/msword',
                'gif' => 'image/gif',
                'gz' => 'application/x-gzip',
                'hqx' => 'application/mac-binhex40',
                'html?' => 'text/html',
                'jar' => 'application/java-archive',
                'jpe?g|jfif?' => 'image/jpeg',
                'js' => 'application/x-javascript',
                'midi' => 'audio/x-midi',
                'mp3' => 'audio/mpeg',
                'mpe?g' => 'video/mpeg',
                'ogg' => 'audio/vorbis',
                'pdf' => 'application/pdf',
                'png' => 'image/png',
                'ppt' => 'application/vnd.ms-powerpoint',
                'ps' => 'application/postscript',
                'qt' => 'video/quicktime',
                'ram?' => 'audio/x-pn-realaudio',
                'rdf' => 'application/rdf',
                'rtf' => 'application/rtf',
                'sgml?' => 'text/sgml',
                'sit' => 'application/x-stuffit',
                'svg' => 'image/svg+xml',
                'swf' => 'application/x-shockwave-flash',
                'tgz' => 'application/x-tar',
                'tiff' => 'image/tiff',
                'txt' => 'text/plain',
                'wav' => 'audio/wav',
                'xls' => 'application/vnd.ms-excel',
                'xml' => 'application/xml',
                'zip' => 'application/x-zip-compressed'
            ];
            foreach ($map as $key => $val) {
                if (preg_match('/' . $key . '/', strtolower($ext[0]))) {
                    return $val;
                }
            }
        }
        return 'application/octet-stream';
    }

    /**
    *    Return the MIME types stated in the HTTP Accept header as an array;
    *    If a list of MIME types is specified, return the best match; or
    *    FALSE if none found
    *    @return array|string|FALSE
    *    @param $list string|array
    **/
    public function acceptable($list = null)
    {
        $accept = [];
        foreach (explode(',', str_replace(' ', '', @$_SERVER['HTTP_ACCEPT'])) as $mime) {
            if (preg_match('/(.+?)(?:;q=([\d\.]+)|$)/', $mime, $parts)) {
                $accept[$parts[1]] = isset($parts[2]) ? $parts[2] : 1;
            }
        }
        if (!$accept) {
            $accept['*/*'] = 1;
        } else {
            krsort($accept);
            arsort($accept);
        }
        if ($list) {
            if (is_string($list)) {
                $list = explode(',', $list);
            }
            foreach ($accept as $mime => $q) {
                if ($q && $out = preg_grep('/' .
                    str_replace('\*', '.*', preg_quote($mime, '/')) . '/', $list)
                ) {
                    return current($out);
                }
            }
            return false;
        }
        return $accept;
    }

    /**
    *    Transmit file to HTTP client; Return file size if successful,
    *    FALSE otherwise
    *    @return int|FALSE
    *    @param $file string
    *    @param $mime string
    *    @param $kbps int
    *    @param $force bool
    *    @param $name string
    *    @param $flush bool
    **/
    public function send($file, $mime = null, $kbps = 0, $force = true, $name = null, $flush = true)
    {
        if (!is_file($file)) {
            return false;
        }
        $size = filesize($file);
        if (PHP_SAPI != 'cli') {
            header('Content-Type: ' . ($mime ?: $this->mime($file)));
            if ($force) {
                header('Content-Disposition: attachment; ' .
                 'filename="' . ($name !== null ? $name : basename($file)) . '"');
            }
            header('Accept-Ranges: bytes');
            header('Content-Length: ' . $size);
            header('X-Powered-By: ' . Base::instance()->PACKAGE);
        }
        if (!$kbps && $flush) {
            while (ob_get_level()) {
                ob_end_clean();
            }
            readfile($file);
        } else {
            $ctr = 0;
            $handle = fopen($file, 'rb');
            $start = microtime(true);
            while (!feof($handle) &&
                ($info = stream_get_meta_data($handle)) &&
                !$info['timed_out'] && !connection_aborted()
            ) {
                if ($kbps) {
                    // Throttle output
                    ++$ctr;
                    if ($ctr / $kbps > $elapsed = microtime(true) - $start) {
                        usleep(round(1e6 * ($ctr / $kbps - $elapsed)));
                    }
                }
                // Send 1KiB and reset timer
                echo fread($handle, 1024);
                if ($flush) {
                    ob_flush();
                    flush();
                }
            }
            fclose($handle);
        }
        return $size;
    }

    /**
    *    Receive file(s) from HTTP client
    *    @return array|bool
    *    @param $func callback
    *    @param $overwrite bool
    *    @param $slug callback|bool
    **/
    public function receive($func = null, $overwrite = false, $slug = true)
    {
        $fw = Base::instance();
        $dir = $fw->UPLOADS;
        if (!is_dir($dir)) {
            mkdir($dir, Base::MODE, true);
        }
        if ($fw->VERB == 'PUT') {
            $tmp = $fw->TEMP . $fw->SEED . '.' . $fw->hash(uniqid());
            if (!$fw->RAW) {
                $fw->write($tmp, $fw->BODY);
            } else {
                $src = @fopen('php://input', 'r');
                $dst = @fopen($tmp, 'w');
                if (!$src || !$dst) {
                    return false;
                }
                while (!feof($src) &&
                    ($info = stream_get_meta_data($src)) &&
                    !$info['timed_out'] && $str = fgets($src, 4096)
                ) {
                    fputs($dst, $str, strlen($str));
                }
                fclose($dst);
                fclose($src);
            }
            $base = basename($fw->URI);
            $file = [
             'name' => $dir .
                 ($slug && preg_match('/(.+?)(\.\w+)?$/', $base, $parts) ?
                     (is_callable($slug) ?
                         $slug($base) :
                         ($this->slug($parts[1]) .
                             (isset($parts[2]) ? $parts[2] : ''))) :
                     $base),
                'tmp_name' => $tmp,
                'type' => $this->mime($base),
                'size' => filesize($tmp)
            ];
            return (!file_exists($file['name']) || $overwrite) &&
             (!$func || $fw->call($func, [$file]) !== false) &&
             rename($tmp, $file['name']);
        }
        $fetch = function ($arr) use (&$fetch) {
            if (!is_array($arr)) {
                return [$arr];
            }
            $data = [];
            foreach ($arr as $k => $sub) {
                $data = array_merge($data, $fetch($sub));
            }
            return $data;
        };
        $out = [];
        foreach ($_FILES as $name => $item) {
            $files = [];
            foreach ($item as $k => $mix) {
                foreach ($fetch($mix) as $i => $val) {
                    $files[$i][$k] = $val;
                }
            }
            foreach ($files as $file) {
                if (empty($file['name'])) {
                    continue;
                }
                $base = basename($file['name']);
                $file['name'] = $dir .
                 ($slug && preg_match('/(.+?)(\.\w+)?$/', $base, $parts) ?
                     (is_callable($slug) ?
                         $slug($base, $name) :
                         ($this->slug($parts[1]) .
                             (isset($parts[2]) ? $parts[2] : ''))) :
                     $base);
                $out[$file['name']] = !$file['error'] &&
                    (!file_exists($file['name']) || $overwrite) &&
                    (!$func || $fw->call($func, [$file,$name]) !== false) &&
                    move_uploaded_file($file['tmp_name'], $file['name']);
            }
        }
        return $out;
    }

    /**
    *    Return upload progress in bytes, FALSE on failure
    *    @return int|FALSE
    *    @param $id string
    **/
    public function progress($id)
    {
     // ID returned by session.upload_progress.name
        return ini_get('session.upload_progress.enabled') &&
         isset($_SESSION[$id]['bytes_processed']) ?
             $_SESSION[$id]['bytes_processed'] : false;
    }

    /**
    *    HTTP request via cURL
    *    @return array
    *    @param $url string
    *    @param $options array
    **/
    protected function curl($url, $options)
    {
        $curl = curl_init($url);
        if (!$open_basedir = ini_get('open_basedir')) {
            curl_setopt(
                $curl,
                CURLOPT_FOLLOWLOCATION,
                $options['follow_location']
            );
        }
        curl_setopt(
            $curl,
            CURLOPT_MAXREDIRS,
            $options['max_redirects']
        );
        curl_setopt($curl, CURLOPT_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        curl_setopt($curl, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS);
        curl_setopt($curl, CURLOPT_CUSTOMREQUEST, $options['method']);
        if (isset($options['header'])) {
            curl_setopt($curl, CURLOPT_HTTPHEADER, $options['header']);
        }
        if (isset($options['content'])) {
            curl_setopt($curl, CURLOPT_POSTFIELDS, $options['content']);
        }
        if (isset($options['proxy'])) {
            curl_setopt($curl, CURLOPT_PROXY, $options['proxy']);
        }
        curl_setopt($curl, CURLOPT_ENCODING, isset($options['encoding'])
         ? $options['encoding'] : 'gzip,deflate');
        $timeout = isset($options['timeout']) ?
         $options['timeout'] :
         ini_get('default_socket_timeout');
        curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, $timeout);
        curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
        $headers = [];
        curl_setopt(
            $curl,
            CURLOPT_HEADERFUNCTION,
            // Callback for response headers
            function ($curl, $line) use (&$headers) {
                if ($trim = trim($line)) {
                    $headers[] = $trim;
                }
                return strlen($line);
            }
        );
        curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
        ob_start();
        curl_exec($curl);
        $err = curl_error($curl);
        curl_close($curl);
        $body = ob_get_clean();
        if (!$err &&
            $options['follow_location'] && $open_basedir &&
            preg_grep('/HTTP\/[\d.]{1,3} 3\d{2}/', $headers) &&
            preg_match('/^Location: (.+)$/m', implode(PHP_EOL, $headers), $loc)
        ) {
            --$options['max_redirects'];
            if ($loc[1][0] == '/') {
                $parts = parse_url($url);
                $loc[1] = $parts['scheme'] . '://' . $parts['host'] .
                 ((isset($parts['port']) && !in_array($parts['port'], [80,443]))
                     ? ':' . $parts['port'] : '') . $loc[1];
            }
            return $this->request($loc[1], $options);
        }
        return [
         'body' => $body,
         'headers' => $headers,
         'engine' => 'cURL',
         'cached' => false,
         'error' => $err
        ];
    }

    /**
    *    HTTP request via PHP stream wrapper
    *    @return array
    *    @param $url string
    *    @param $options array
    **/
    protected function stream($url, $options)
    {
        $eol = "\r\n";
        if (isset($options['proxy'])) {
            $options['proxy'] = preg_replace('/https?/i', 'tcp', $options['proxy']);
            $options['request_fulluri'] = true;
            if (preg_match('/socks4?/i', $options['proxy'])) {
                return $this->_socket($url, $options);
            }
        }
        $options['header'] = implode($eol, $options['header']);
        $body = @file_get_contents(
            $url,
            false,
            stream_context_create(['http' => $options])
        );
        $headers = isset($http_response_header) ?
         $http_response_header : [];
        $err = '';
        if (is_string($body)) {
            $match = null;
            foreach ($headers as $header) {
                if (preg_match('/Content-Encoding: (.+)/i', $header, $match)) {
                    break;
                }
            }
            if ($match) {
                switch ($match[1]) {
                    case 'gzip':
                        $body = gzdecode($body);
                        break;
                    case 'deflate':
                        $body = gzuncompress($body);
                        break;
                }
            }
        } else {
            $tmp = error_get_last();
            $err = $tmp['message'];
        }
        return [
         'body' => $body,
         'headers' => $headers,
         'engine' => 'stream',
         'cached' => false,
         'error' => $err
        ];
    }

    /**
    *    HTTP request via low-level TCP/IP socket
    *    @return array
    *    @param $url string
    *    @param $options array
    **/
    protected function socket($url, $options)
    {
        $eol = "\r\n";
        $headers = [];
        $body = '';
        $parts = parse_url($url);
        $hostname = $parts['host'];
        $proxy = false;
        if ($parts['scheme'] == 'https') {
            $parts['host'] = 'ssl://' . $parts['host'];
        }
        if (empty($parts['port'])) {
            $parts['port'] = $parts['scheme'] == 'https' ? 443 : 80;
        }
        if (empty($parts['path'])) {
            $parts['path'] = '/';
        }
        if (empty($parts['query'])) {
            $parts['query'] = '';
        }
        if (isset($options['proxy'])) {
            $req = $url;
            $pp = parse_url($options['proxy']);
            $proxy = $pp['scheme'];
            if ($pp['scheme'] == 'https') {
                $pp['host'] = 'ssl://' . $pp['host'];
            }
            if (empty($pp['port'])) {
                $pp['port'] = $pp['scheme'] == 'https' ? 443 : 80;
            }
            $socket = @fsockopen($pp['host'], $pp['port'], $code, $err);
        } else {
            $req = $parts['path'] . ($parts['query'] ? ('?' . $parts['query']) : '');
            $socket = @fsockopen($parts['host'], $parts['port'], $code, $err);
        }
        if ($socket) {
            stream_set_blocking($socket, true);
            stream_set_timeout($socket, isset($options['timeout']) ?
             $options['timeout'] : ini_get('default_socket_timeout'));
            if ($proxy == 'socks4') {
                // SOCKS4; http://en.wikipedia.org/wiki/SOCKS#Protocol
                $packet = "\x04\x01" . pack("n", $parts['port']) .
                    pack("H*", dechex(ip2long(gethostbyname($hostname)))) . "\0";
                fputs($socket, $packet, strlen($packet));
                $response = fread($socket, 9);
                if (strlen($response) == 8 && (ord($response[0]) == 0 || ord($response[0]) == 4)
                    && ord($response[1]) == 90
                ) {
                    $options['header'][] = 'Host: ' . $hostname;
                } else {
                    $err = 'Socket Status ' . ord($response[1]);
                }
            }
            fputs($socket, $options['method'] . ' ' . $req . ' HTTP/1.0' . $eol);
            fputs($socket, implode($eol, $options['header']) . $eol . $eol);
            if (isset($options['content'])) {
                fputs($socket, $options['content'] . $eol);
            }
            // Get response
            $content = '';
            while (!feof($socket) &&
                ($info = stream_get_meta_data($socket)) &&
                !$info['timed_out'] && !connection_aborted() &&
                $str = fgets($socket, 4096)
            ) {
                $content .= $str;
            }
            fclose($socket);
            $html = explode($eol . $eol, $content, 2);
            $body = isset($html[1]) ? $html[1] : '';
            $headers = array_merge($headers, $current = explode($eol, $html[0]));
            $match = null;
            foreach ($current as $header) {
                if (preg_match('/Content-Encoding: (.+)/i', $header, $match)) {
                    break;
                }
            }
            if ($match) {
                switch ($match[1]) {
                    case 'gzip':
                        $body = gzdecode($body);
                        break;
                    case 'deflate':
                        $body = gzuncompress($body);
                        break;
                }
            }
            if ($options['follow_location'] &&
                preg_grep('/HTTP\/[\d.]{1,3} 3\d{2}/', $headers) &&
                preg_match(
                    '/Location: (.+?)' . preg_quote($eol) . '/',
                    $html[0],
                    $loc
                )
            ) {
                --$options['max_redirects'];
                return $this->request($loc[1], $options);
            }
        }
        return [
         'body' => $body,
         'headers' => $headers,
         'engine' => 'socket',
         'cached' => false,
         'error' => $err
        ];
    }

    /**
    *    Specify the HTTP request engine to use; If not available,
    *    fall back to an applicable substitute
    *    @return string
    *    @param $arg string
    **/
    public function engine($arg = 'curl')
    {
        $arg = strtolower($arg);
        $flags = [
         'curl' => extension_loaded('curl'),
         'stream' => ini_get('allow_url_fopen'),
         'socket' => function_exists('fsockopen')
        ];
        if ($flags[$arg]) {
            return $this->wrapper = $arg;
        }
        foreach ($flags as $key => $val) {
            if ($val) {
                return $this->wrapper = $key;
            }
        }
        user_error(self::E_Request, E_USER_ERROR);
    }

    /**
    *    Replace old headers with new elements
    *    @return NULL
    *    @param $old array
    *    @param $new string|array
    **/
    public function subst(array &$old, $new)
    {
        if (is_string($new)) {
            $new = [$new];
        }
        foreach ($new as $hdr) {
            $old = preg_grep(
                '/' . preg_quote(strstr($hdr, ':', true), '/') . ':.+/',
                $old,
                PREG_GREP_INVERT
            );
            array_push($old, $hdr);
        }
    }

    /**
    *    Submit HTTP request; Use HTTP context options (described in
    *    http://www.php.net/manual/en/context.http.php) if specified;
    *    Cache the page as instructed by remote server
    *    @return array|FALSE
    *    @param $url string
    *    @param $options array
    **/
    public function request($url, array $options = null)
    {
        $fw = Base::instance();
        $parts = parse_url($url);
        if (empty($parts['scheme'])) {
            // Local URL
            $url = $fw->SCHEME . '://' . $fw->HOST .
             (in_array($fw->PORT, [80,443]) ? '' : (':' . $fw->PORT)) .
             ($url[0] != '/' ? ($fw->BASE . '/') : '') . $url;
            $parts = parse_url($url);
        } elseif (!preg_match('/https?/', $parts['scheme'])) {
            return false;
        }
        if (!is_array($options)) {
            $options = [];
        }
        if (empty($options['header'])) {
            $options['header'] = [];
        } elseif (is_string($options['header'])) {
            $options['header'] = [$options['header']];
        }
        if (!$this->wrapper) {
            $this->engine();
        }
        if ($this->wrapper != 'stream') {
            // PHP streams can't cope with redirects when Host header is set
            $this->subst($options['header'], 'Host: ' . $parts['host']);
        }
        $this->subst(
            $options['header'],
            [
             'Accept-Encoding: ' . (isset($options['encoding']) ?
                 $options['encoding'] : 'gzip,deflate'),
             'User-Agent: ' . (isset($options['user_agent']) ?
                 $options['user_agent'] :
                 'Mozilla/5.0 (compatible; ' . php_uname('s') . ')'),
             'Connection: close'
            ]
        );
        if (isset($options['content']) && is_string($options['content'])) {
            if ($options['method'] == 'POST' &&
                !preg_grep('/^Content-Type:/i', $options['header'])
            ) {
                $this->subst(
                    $options['header'],
                    'Content-Type: application/x-www-form-urlencoded'
                );
            }
            $this->subst(
                $options['header'],
                'Content-Length: ' . strlen($options['content'])
            );
        }
        if (isset($parts['user'], $parts['pass'])) {
            $this->subst(
                $options['header'],
                'Authorization: Basic ' .
                 base64_encode($parts['user'] . ':' . $parts['pass'])
            );
        }
        $options += [
         'method' => 'GET',
         'header' => $options['header'],
         'follow_location' => true,
         'max_redirects' => 20,
         'ignore_errors' => false
        ];
        $eol = "\r\n";
        if ($fw->CACHE &&
            preg_match('/GET|HEAD/', $options['method'])
        ) {
            $cache = Cache::instance();
            if ($cache->exists(
                $hash = $fw->hash($options['method'] . ' ' . $url) . '.url',
                $data
            )
            ) {
                if (preg_match(
                    '/Last-Modified: (.+?)' . preg_quote($eol) . '/',
                    implode($eol, $data['headers']),
                    $mod
                )
                ) {
                    $this->subst(
                        $options['header'],
                        'If-Modified-Since: ' . $mod[1]
                    );
                }
            }
        }
        $result = $this->{$this->wrapper}($url, $options);
        if ($result && isset($cache)) {
            if (preg_match(
                '/HTTP\/[\d.]{1,3} 304/',
                implode($eol, $result['headers'])
            )
            ) {
                $result = $cache->get($hash);
                $result['cached'] = true;
            } elseif (preg_match('/Cache-Control:(?:.*)max-age=(\d+)(?:,?.*' .
                preg_quote($eol) . ')/i', implode($eol, $result['headers']), $exp)
            ) {
                $cache->set($hash, $result, $exp[1]);
            }
        }
        $req = [$options['method'] . ' ' . $url];
        foreach ($options['header'] as $header) {
            array_push($req, $header);
        }
        return array_merge(['request' => $req], $result);
    }

 /**
 *   Strip Javascript/CSS files of extraneous whitespaces and comments;
 *   Return combined output as a minified string
 *   @return string
 *   @param $files string|array
 *   @param $mime string
 *   @param $header bool
 *   @param $path string
 **/
    public function minify($files, $mime = null, $header = true, $path = null)
    {
        $fw = Base::instance();
        if (is_string($files)) {
            $files = $fw->split($files);
        }
        if (!$mime) {
            $mime = $this->mime($files[0]);
        }
        preg_match('/\w+$/', $files[0], $ext);
        $cache = Cache::instance();
        $dst = '';
        if (!isset($path)) {
            $path = $fw->UI . ';./';
        }
        foreach (array_unique($fw->split($path, false)) as $dir) {
            foreach ($files as $i => $file) {
                if (is_file($save = $fw->fixslashes($dir . $file)) &&
                    is_bool(strpos($save, '../')) &&
                    preg_match('/\.(css|js)$/i', $file)
                ) {
                    unset($files[$i]);
                    if ($fw->CACHE &&
                        ($cached = $cache->exists(
                            $hash = $fw->hash($save) . '.' . $ext[0],
                            $data
                        )) &&
                        $cached[0] > filemtime($save)
                    ) {
                        $dst .= $data;
                    } else {
                        $data = '';
                        $src = $fw->read($save);
                        for ($ptr = 0,$len = strlen($src); $ptr < $len;) {
                            if (preg_match(
                                '/^@import\h+url' .
                                    '\(\h*([\'"])((?!(?:https?:)?\/\/).+?)\1\h*\)[^;]*;/',
                                substr($src, $ptr),
                                $parts
                            )
                            ) {
                                $path = dirname($file);
                                $data .= $this->minify(
                                    ($path ? ($path . '/') : '') . $parts[2],
                                    $mime,
                                    $header
                                );
                                $ptr += strlen($parts[0]);
                                continue;
                            }
                            if ($ext[0] == 'css' && preg_match(
                                '/^url\(([^\'"].*?[^\'"])\)/i',
                                substr($src, $ptr),
                                $parts
                            )
                            ) {
                                $data .= $parts[0];
                                $ptr += strlen($parts[0]);
                                continue;
                            }
                            if ($src[$ptr] == '/') {
                                if ($src[$ptr + 1] == '*') {
                                    // Multiline comment
                                    $str = strstr(
                                        substr($src, $ptr + 2),
                                        '*/',
                                        true
                                    );
                                    $ptr += strlen($str) + 4;
                                } elseif ($src[$ptr + 1] == '/') {
                                    // Single-line comment
                                    $str = strstr(
                                        substr($src, $ptr + 2),
                                        "\n",
                                        true
                                    );
                                    $ptr += (empty($str)) ?
                                    strlen(substr($src, $ptr)) : strlen($str) + 2;
                                } else {
                                    // Presume it's a regex pattern
                                    $regex = true;
                                    // Backtrack and validate
                                    for ($ofs = $ptr; $ofs; --$ofs) {
                                        // Pattern should be preceded by
                                        // open parenthesis, colon,
                                        // object property or operator
                                        if (preg_match(
                                            '/(return|[(:=!+\-*&|])$/',
                                            substr($src, 0, $ofs)
                                        )
                                        ) {
                                            $data .= '/';
                                            ++$ptr;
                                            while ($ptr < $len) {
                                                $data .= $src[$ptr];
                                                ++$ptr;
                                                if ($src[$ptr - 1] == '\\') {
                                                    $data .= $src[$ptr];
                                                    ++$ptr;
                                                } elseif ($src[$ptr - 1] == '/') {
                                                    break;
                                                }
                                            }
                                            break;
                                        } elseif (!ctype_space($src[$ofs - 1])) {
                                            // Not a regex pattern
                                            $regex = false;
                                            break;
                                        }
                                    }
                                    if (!$regex) {
                                        // Division operator
                                        $data .= $src[$ptr];
                                        ++$ptr;
                                    }
                                }
                                continue;
                            }
                            if (in_array($src[$ptr], ['\'','"','`'])) {
                                $match = $src[$ptr];
                                $data .= $match;
                                ++$ptr;
                                // String literal
                                while ($ptr < $len) {
                                    $data .= $src[$ptr];
                                    ++$ptr;
                                    if ($src[$ptr - 1] == '\\') {
                                        $data .= $src[$ptr];
                                        ++$ptr;
                                    } elseif ($src[$ptr - 1] == $match) {
                                        break;
                                    }
                                }
                                continue;
                            }
                            if (ctype_space($src[$ptr])) {
                                if ($ptr + 1 < strlen($src) &&
                                    preg_match(
                                        '/[\w' . ($ext[0] == 'css' ?
                                        '#\.%+\-*()\[\]' : '\$') . ']{2}|' .
                                        '[+\-]{2}/',
                                        substr($data, -1) . $src[$ptr + 1]
                                    ) ||
                                    ($ext[0] == 'css' && $ptr + 2 < strlen($src) &&
                                    preg_match('/:\w/', substr($src, $ptr + 1, 2)))
                                ) {
                                    $data .= ' ';
                                }
                                ++$ptr;
                                continue;
                            }
                            $data .= $src[$ptr];
                            ++$ptr;
                        }
                        if ($ext[0] == 'css') {
                            $data = str_replace(';}', '}', $data);
                        }
                        if ($fw->CACHE) {
                            $cache->set($hash, $data);
                        }
                        $dst .= $data;
                    }
                }
            }
        }
        if (PHP_SAPI != 'cli' && $header) {
            header('Content-Type: ' . $mime . '; charset=' . $fw->ENCODING);
        }
        return $dst;
    }

 /**
 *   Retrieve RSS feed and return as an array
 *   @return array|FALSE
 *   @param $url string
 *   @param $max int
 *   @param $tags string
 **/
    public function rss($url, $max = 10, $tags = null)
    {
        if (!$data = $this->request($url)) {
            return false;
        }
        // Suppress errors caused by invalid XML structures
        libxml_use_internal_errors(true);
        $xml = simplexml_load_string(
            $data['body'],
            null,
            LIBXML_NOBLANKS | LIBXML_NOERROR
        );
        if (!is_object($xml)) {
            return false;
        }
        $out = [];
        if (isset($xml->channel)) {
            $out['source'] = (string)$xml->channel->title;
            $max = min($max, count($xml->channel->item));
            for ($i = 0; $i < $max; ++$i) {
                $item = $xml->channel->item[$i];
                $list = ['' => null] + $item->getnamespaces(true);
                $fields = [];
                foreach ($list as $ns => $uri) {
                    foreach ($item->children($uri) as $key => $val) {
                        $fields[$ns . ($ns ? ':' : '') . $key] = (string)$val;
                    }
                }
                $out['feed'][] = $fields;
            }
        } else {
            return false;
        }
        Base::instance()->scrub($out, $tags);
        return $out;
    }

 /**
 *   Retrieve information from whois server
 *   @return string|FALSE
 *   @param $addr string
 *   @param $server string
 **/
    public function whois($addr, $server = 'whois.internic.net')
    {
        $socket = @fsockopen($server, 43, $errno, $errstr);
        if (!$socket) {
         // Can't establish connection
            return false;
        }
        // Set connection timeout parameters
        stream_set_blocking($socket, false);
        stream_set_timeout($socket, ini_get('default_socket_timeout'));
        // Send request
        fputs($socket, $addr . "\r\n");
        $info = stream_get_meta_data($socket);
        // Get response
        $response = '';
        while (!feof($socket) && !$info['timed_out']) {
            $response .= fgets($socket, 4096); // MDFK97
            $info = stream_get_meta_data($socket);
        }
        fclose($socket);
        return $info['timed_out'] ? false : trim($response);
    }

 /**
 *   Return preset diacritics translation table
 *   @return array
 **/
    public function diacritics()
    {
        return [
         'Ǎ' => 'A','А' => 'A','Ā' => 'A','Ă' => 'A','Ą' => 'A','Å' => 'A',
         'Ǻ' => 'A','Ä' => 'Ae','Á' => 'A','À' => 'A','Ã' => 'A','Â' => 'A',
         'Æ' => 'AE','Ǽ' => 'AE','Б' => 'B','Ç' => 'C','Ć' => 'C','Ĉ' => 'C',
         'Č' => 'C','Ċ' => 'C','Ц' => 'C','Ч' => 'Ch','Ð' => 'Dj','Đ' => 'Dj',
         'Ď' => 'Dj','Д' => 'Dj','É' => 'E','Ę' => 'E','Ё' => 'E','Ė' => 'E',
         'Ê' => 'E','Ě' => 'E','Ē' => 'E','È' => 'E','Е' => 'E','Э' => 'E',
         'Ë' => 'E','Ĕ' => 'E','Ф' => 'F','Г' => 'G','Ģ' => 'G','Ġ' => 'G',
         'Ĝ' => 'G','Ğ' => 'G','Х' => 'H','Ĥ' => 'H','Ħ' => 'H','Ï' => 'I',
         'Ĭ' => 'I','İ' => 'I','Į' => 'I','Ī' => 'I','Í' => 'I','Ì' => 'I',
         'И' => 'I','Ǐ' => 'I','Ĩ' => 'I','Î' => 'I','Ĳ' => 'IJ','Ĵ' => 'J',
         'Й' => 'J','Я' => 'Ja','Ю' => 'Ju','К' => 'K','Ķ' => 'K','Ĺ' => 'L',
         'Л' => 'L','Ł' => 'L','Ŀ' => 'L','Ļ' => 'L','Ľ' => 'L','М' => 'M',
         'Н' => 'N','Ń' => 'N','Ñ' => 'N','Ņ' => 'N','Ň' => 'N','Ō' => 'O',
         'О' => 'O','Ǿ' => 'O','Ǒ' => 'O','Ơ' => 'O','Ŏ' => 'O','Ő' => 'O',
         'Ø' => 'O','Ö' => 'Oe','Õ' => 'O','Ó' => 'O','Ò' => 'O','Ô' => 'O',
         'Œ' => 'OE','П' => 'P','Ŗ' => 'R','Р' => 'R','Ř' => 'R','Ŕ' => 'R',
         'Ŝ' => 'S','Ş' => 'S','Š' => 'S','Ș' => 'S','Ś' => 'S','С' => 'S',
         'Ш' => 'Sh','Щ' => 'Shch','Ť' => 'T','Ŧ' => 'T','Ţ' => 'T','Ț' => 'T',
         'Т' => 'T','Ů' => 'U','Ű' => 'U','Ŭ' => 'U','Ũ' => 'U','Ų' => 'U',
         'Ū' => 'U','Ǜ' => 'U','Ǚ' => 'U','Ù' => 'U','Ú' => 'U','Ü' => 'Ue',
         'Ǘ' => 'U','Ǖ' => 'U','У' => 'U','Ư' => 'U','Ǔ' => 'U','Û' => 'U',
         'В' => 'V','Ŵ' => 'W','Ы' => 'Y','Ŷ' => 'Y','Ý' => 'Y','Ÿ' => 'Y',
         'Ź' => 'Z','З' => 'Z','Ż' => 'Z','Ž' => 'Z','Ж' => 'Zh','á' => 'a',
         'ă' => 'a','â' => 'a','à' => 'a','ā' => 'a','ǻ' => 'a','å' => 'a',
         'ä' => 'ae','ą' => 'a','ǎ' => 'a','ã' => 'a','а' => 'a','ª' => 'a',
         'æ' => 'ae','ǽ' => 'ae','б' => 'b','č' => 'c','ç' => 'c','ц' => 'c',
         'ċ' => 'c','ĉ' => 'c','ć' => 'c','ч' => 'ch','ð' => 'dj','ď' => 'dj',
         'д' => 'dj','đ' => 'dj','э' => 'e','é' => 'e','ё' => 'e','ë' => 'e',
         'ê' => 'e','е' => 'e','ĕ' => 'e','è' => 'e','ę' => 'e','ě' => 'e',
         'ė' => 'e','ē' => 'e','ƒ' => 'f','ф' => 'f','ġ' => 'g','ĝ' => 'g',
         'ğ' => 'g','г' => 'g','ģ' => 'g','х' => 'h','ĥ' => 'h','ħ' => 'h',
         'ǐ' => 'i','ĭ' => 'i','и' => 'i','ī' => 'i','ĩ' => 'i','į' => 'i',
         'ı' => 'i','ì' => 'i','î' => 'i','í' => 'i','ï' => 'i','ĳ' => 'ij',
         'ĵ' => 'j','й' => 'j','я' => 'ja','ю' => 'ju','ķ' => 'k','к' => 'k',
         'ľ' => 'l','ł' => 'l','ŀ' => 'l','ĺ' => 'l','ļ' => 'l','л' => 'l',
         'м' => 'm','ņ' => 'n','ñ' => 'n','ń' => 'n','н' => 'n','ň' => 'n',
         'ŉ' => 'n','ó' => 'o','ò' => 'o','ǒ' => 'o','ő' => 'o','о' => 'o',
         'ō' => 'o','º' => 'o','ơ' => 'o','ŏ' => 'o','ô' => 'o','ö' => 'oe',
         'õ' => 'o','ø' => 'o','ǿ' => 'o','œ' => 'oe','п' => 'p','р' => 'r',
         'ř' => 'r','ŕ' => 'r','ŗ' => 'r','ſ' => 's','ŝ' => 's','ș' => 's',
         'š' => 's','ś' => 's','с' => 's','ş' => 's','ш' => 'sh','щ' => 'shch',
         'ß' => 'ss','ţ' => 't','т' => 't','ŧ' => 't','ť' => 't','ț' => 't',
         'у' => 'u','ǘ' => 'u','ŭ' => 'u','û' => 'u','ú' => 'u','ų' => 'u',
         'ù' => 'u','ű' => 'u','ů' => 'u','ư' => 'u','ū' => 'u','ǚ' => 'u',
         'ǜ' => 'u','ǔ' => 'u','ǖ' => 'u','ũ' => 'u','ü' => 'ue','в' => 'v',
         'ŵ' => 'w','ы' => 'y','ÿ' => 'y','ý' => 'y','ŷ' => 'y','ź' => 'z',
         'ž' => 'z','з' => 'z','ż' => 'z','ж' => 'zh','ь' => '','ъ' => '',
         'њ' => 'nj','љ' => 'lj','ђ' => 'dj','џ' => 'dz','ћ' => 'c','ј' => 'j',
         '\'' => '',
        ];
    }

 /**
 *   Return a URL/filesystem-friendly version of string
 *   @return string
 *   @param $text string
 **/
    public function slug($text)
    {
        return trim(strtolower(preg_replace(
            '/([^\pL\pN])+/u',
            '-',
            trim(strtr($text, Base::instance()->DIACRITICS + $this->diacritics()))
        )), '-');
    }

 /**
 *   Return chunk of text from standard Lorem Ipsum passage
 *   @return string
 *   @param $count int
 *   @param $max int
 *   @param $std bool
 **/
    public function filler($count = 1, $max = 20, $std = true)
    {
        $out = '';
        if ($std) {
            $out = 'Lorem ipsum dolor sit amet, consectetur adipisicing elit, ' .
             'sed do eiusmod tempor incididunt ut labore et dolore magna ' .
             'aliqua.';
        }
        $rnd = explode(
            ' ',
            'a ab ad accusamus adipisci alias aliquam amet animi aperiam ' .
            'architecto asperiores aspernatur assumenda at atque aut beatae ' .
            'blanditiis cillum commodi consequatur corporis corrupti culpa ' .
            'cum cupiditate debitis delectus deleniti deserunt dicta ' .
            'dignissimos distinctio dolor ducimus duis ea eaque earum eius ' .
            'eligendi enim eos error esse est eum eveniet ex excepteur ' .
            'exercitationem expedita explicabo facere facilis fugiat harum ' .
            'hic id illum impedit in incidunt ipsa iste itaque iure iusto ' .
            'laborum laudantium libero magnam maiores maxime minim minus ' .
            'modi molestiae mollitia nam natus necessitatibus nemo neque ' .
            'nesciunt nihil nisi nobis non nostrum nulla numquam occaecati ' .
            'odio officia omnis optio pariatur perferendis perspiciatis ' .
            'placeat porro possimus praesentium proident quae quia quibus ' .
            'quo ratione recusandae reiciendis rem repellat reprehenderit ' .
            'repudiandae rerum saepe sapiente sequi similique sint soluta ' .
            'suscipit tempora tenetur totam ut ullam unde vel veniam vero ' .
            'vitae voluptas'
        );
        for ($i = 0,$add = $count - (int)$std; $i < $add; ++$i) {
            shuffle($rnd);
            $words = array_slice($rnd, 0, mt_rand(3, $max));
            $out .= (!$std && $i == 0 ? '' : ' ') . ucfirst(implode(' ', $words)) . '.';
        }
        return $out;
    }
}

if (!function_exists('gzdecode')) {

 /**
 *   Decode gzip-compressed string
 *   @param $str string
 *   @return string
 **/
    function gzdecode($str)
    {
        $fw = Base::instance();
        if (!is_dir($tmp = $fw->TEMP)) {
            mkdir($tmp, Base::MODE, true);
        }
        file_put_contents($file = $tmp . '/' . $fw->SEED . '.' .
         $fw->hash(uniqid('', true)) . '.gz', $str, LOCK_EX);
        ob_start();
        readgzfile($file);
        $out = ob_get_clean();
        @unlink($file);
        return $out;
    }
}
